@objectstack/runtime 6.8.1 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1468,6 +1468,66 @@ var init_app_plugin = __esm({
1468
1468
  } catch (err) {
1469
1469
  ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1470
1470
  }
1471
+ try {
1472
+ const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1473
+ if (jobs.length > 0) {
1474
+ ctx.hook("kernel:ready", async () => {
1475
+ let svc;
1476
+ try {
1477
+ svc = ctx.getService("job");
1478
+ } catch {
1479
+ }
1480
+ if (!svc || typeof svc.schedule !== "function") {
1481
+ ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
1482
+ appId,
1483
+ jobCount: jobs.length
1484
+ });
1485
+ return;
1486
+ }
1487
+ const fnMap = collectBundleFunctions(this.bundle);
1488
+ let ok = 0;
1489
+ for (const job of jobs) {
1490
+ const jobName = job?.name;
1491
+ if (!jobName) {
1492
+ ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
1493
+ continue;
1494
+ }
1495
+ if (job.enabled === false) {
1496
+ ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
1497
+ continue;
1498
+ }
1499
+ const handler = fnMap[job.handler];
1500
+ if (typeof handler !== "function") {
1501
+ ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
1502
+ appId,
1503
+ job: jobName,
1504
+ handler: job.handler
1505
+ });
1506
+ continue;
1507
+ }
1508
+ try {
1509
+ await svc.schedule(
1510
+ jobName,
1511
+ job.schedule,
1512
+ async (jobCtx) => {
1513
+ await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
1514
+ }
1515
+ );
1516
+ ok++;
1517
+ } catch (err) {
1518
+ ctx.logger.warn("[AppPlugin] Failed to schedule job", {
1519
+ appId,
1520
+ job: jobName,
1521
+ error: err?.message ?? String(err)
1522
+ });
1523
+ }
1524
+ }
1525
+ ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
1526
+ });
1527
+ }
1528
+ } catch (err) {
1529
+ ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
1530
+ }
1471
1531
  this.emitCatalogEvent(ctx, "app:registered", sys);
1472
1532
  await this.loadTranslations(ctx, appId);
1473
1533
  const seedDatasets = [];
@@ -1540,7 +1600,7 @@ var init_app_plugin = __esm({
1540
1600
  } catch (e) {
1541
1601
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1542
1602
  }
1543
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
1603
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
1544
1604
  if (multiTenant) {
1545
1605
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1546
1606
  } else {
@@ -1595,10 +1655,22 @@ var init_app_plugin = __esm({
1595
1655
  };
1596
1656
  this.bundle = bundle;
1597
1657
  this.projectContext = projectContext;
1598
- const sys = bundle.manifest || bundle;
1599
- const appId = sys.id || sys.name || "unnamed-app";
1658
+ const sys = bundle?.manifest || bundle;
1659
+ const appId = sys?.id || sys?.name;
1660
+ if (!appId) {
1661
+ const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1662
+ const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1663
+ const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
1664
+ environmentId: projectContext.environmentId,
1665
+ packageId: projectContext.packageId,
1666
+ source: projectContext.source
1667
+ })}` : "";
1668
+ throw new Error(
1669
+ `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1670
+ );
1671
+ }
1600
1672
  this.name = `plugin.app.${appId}`;
1601
- this.version = sys.version;
1673
+ this.version = sys?.version;
1602
1674
  }
1603
1675
  /**
1604
1676
  * Emit a kernel hook so the control-plane `AppCatalogService` can
@@ -2528,8 +2600,12 @@ async function resolveExecutionContext(opts) {
2528
2600
  if (!userId) {
2529
2601
  try {
2530
2602
  const authService = await opts.getService("auth");
2603
+ let api = authService?.api;
2604
+ if (!api && typeof authService?.getApi === "function") {
2605
+ api = await authService.getApi();
2606
+ }
2531
2607
  const headersInstance = toHeaders(headers);
2532
- const sessionData = await authService?.api?.getSession?.({ headers: headersInstance });
2608
+ const sessionData = await api?.getSession?.({ headers: headersInstance });
2533
2609
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2534
2610
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2535
2611
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
@@ -5374,7 +5450,7 @@ var _HttpDispatcher = class _HttpDispatcher {
5374
5450
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
5375
5451
  * Resolves the AI service and its built-in route handlers, then dispatches.
5376
5452
  */
5377
- async handleAI(subPath, method, body, query, _context) {
5453
+ async handleAI(subPath, method, body, query, context) {
5378
5454
  let aiService;
5379
5455
  try {
5380
5456
  aiService = await this.resolveService("ai");
@@ -5418,7 +5494,23 @@ var _HttpDispatcher = class _HttpDispatcher {
5418
5494
  if (route.method !== method) continue;
5419
5495
  const params = matchRoute(route.path, fullPath);
5420
5496
  if (params === null) continue;
5421
- const result = await route.handler({ body, params, query });
5497
+ const ec = context.executionContext;
5498
+ const user = ec?.userId ? {
5499
+ userId: ec.userId,
5500
+ id: ec.userId,
5501
+ displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
5502
+ email: ec.userEmail,
5503
+ roles: Array.isArray(ec.roles) ? ec.roles : [],
5504
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
5505
+ organizationId: ec.tenantId
5506
+ } : void 0;
5507
+ const result = await route.handler({
5508
+ body,
5509
+ params,
5510
+ query,
5511
+ headers: context.request?.headers,
5512
+ user
5513
+ });
5422
5514
  if (result.stream && result.events) {
5423
5515
  return {
5424
5516
  handled: true,
@@ -5913,13 +6005,22 @@ function resolveErrorReporter(ctx, override) {
5913
6005
  }
5914
6006
 
5915
6007
  // src/dispatcher-plugin.ts
5916
- function mountRouteOnServer(route, server, routePath, securityHeaders) {
6008
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5917
6009
  const handler = async (req, res) => {
5918
6010
  try {
6011
+ let user;
6012
+ if (resolveUser) {
6013
+ try {
6014
+ user = await resolveUser(req.headers ?? {});
6015
+ } catch {
6016
+ }
6017
+ }
5919
6018
  const result = await route.handler({
5920
6019
  body: req.body,
5921
6020
  params: req.params,
5922
- query: req.query
6021
+ query: req.query,
6022
+ headers: req.headers,
6023
+ user
5923
6024
  });
5924
6025
  if (result.stream && result.events) {
5925
6026
  res.status(result.status);
@@ -6656,6 +6757,32 @@ function createDispatcherPlugin(config = {}) {
6656
6757
  }
6657
6758
  }
6658
6759
  ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
6760
+ const resolveRequestUser = async (headers) => {
6761
+ try {
6762
+ const authService = ctx.getService("auth");
6763
+ if (!authService) return void 0;
6764
+ let api = authService.api;
6765
+ if (!api && typeof authService.getApi === "function") {
6766
+ api = await authService.getApi();
6767
+ }
6768
+ if (!api?.getSession) return void 0;
6769
+ const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
6770
+ const sessionData = await api.getSession({ headers: headersInstance });
6771
+ const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
6772
+ if (!userId) return void 0;
6773
+ return {
6774
+ userId,
6775
+ id: userId,
6776
+ displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
6777
+ email: sessionData?.user?.email,
6778
+ roles: [],
6779
+ permissions: [],
6780
+ organizationId: sessionData?.session?.activeOrganizationId
6781
+ };
6782
+ } catch {
6783
+ return void 0;
6784
+ }
6785
+ };
6659
6786
  const toScopedPath = (routePath) => {
6660
6787
  if (routePath.startsWith(prefix)) {
6661
6788
  const tail = routePath.slice(prefix.length);
@@ -6668,11 +6795,11 @@ function createDispatcherPlugin(config = {}) {
6668
6795
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6669
6796
  let count = 0;
6670
6797
  if (enableProjectScoping && projectResolution === "required") {
6671
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6798
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6672
6799
  } else {
6673
- if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
6800
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6674
6801
  if (enableProjectScoping) {
6675
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6802
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6676
6803
  }
6677
6804
  }
6678
6805
  return count;
@@ -7734,39 +7861,7 @@ var ArtifactKernelFactory = class {
7734
7861
  // intentionally do NOT pass crossSubDomainCookies here
7735
7862
  // so cookies stay isolated per project subdomain.
7736
7863
  trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7737
- ...oidcProviders ? { oidcProviders } : {},
7738
- // Auto-provision a personal organization for every new
7739
- // user. SecurityPlugin's ObjectQL middleware does this
7740
- // for direct `ql.insert` calls, but better-auth's
7741
- // adapter writes through `dataEngine` directly,
7742
- // bypassing that middleware — so JIT-created SSO users
7743
- // would otherwise land on the empty "create
7744
- // organization" screen on first login.
7745
- databaseHooks: {
7746
- user: {
7747
- create: {
7748
- after: async (user) => {
7749
- try {
7750
- const ql = kernel.getService("objectql");
7751
- if (!ql) return;
7752
- const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
7753
- import("@objectstack/plugin-security")
7754
- ]);
7755
- await ensureUserHasOrganization(ql, user, {
7756
- logger: this.logger,
7757
- cloneSeedData: cloneTenantSeedData
7758
- });
7759
- } catch (e) {
7760
- this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
7761
- environmentId,
7762
- userId: user?.id,
7763
- error: e?.message
7764
- });
7765
- }
7766
- }
7767
- }
7768
- }
7769
- }
7864
+ ...oidcProviders ? { oidcProviders } : {}
7770
7865
  }));
7771
7866
  if (oidcProviders) {
7772
7867
  this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
@@ -7785,7 +7880,7 @@ var ArtifactKernelFactory = class {
7785
7880
  }
7786
7881
  try {
7787
7882
  const { SecurityPlugin } = await import("@objectstack/plugin-security");
7788
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
7883
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7789
7884
  await kernel.use(new SecurityPlugin({ multiTenant }));
7790
7885
  } catch (err) {
7791
7886
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
@@ -7794,8 +7889,15 @@ var ArtifactKernelFactory = class {
7794
7889
  });
7795
7890
  }
7796
7891
  const projectName = project.hostname ?? environmentId;
7797
- const bundle = artifact.metadata;
7798
- const sys = bundle?.manifest ?? bundle;
7892
+ const artifactAny = artifact;
7893
+ const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
7894
+ const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
7895
+ const bundle = {
7896
+ ...artifact.metadata ?? {},
7897
+ ...topLevelManifest ? { manifest: topLevelManifest } : {},
7898
+ functions: topLevelFunctions
7899
+ };
7900
+ const sys = bundle.manifest ?? bundle;
7799
7901
  const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7800
7902
  const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7801
7903
  const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
@@ -9063,7 +9165,7 @@ var MarketplaceInstallLocalPlugin = class {
9063
9165
  }
9064
9166
  }
9065
9167
  if (opts.seedNow && datasets.length > 0) {
9066
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
9168
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
9067
9169
  try {
9068
9170
  const ql = ctx.getService("objectql");
9069
9171
  let metadata;